Fragments

An Activity may comprise one or more fragments, each of which represents a portion of a user interface. Multiple fragments can be combined in a single actiity to build a multi-pane screen. In this lab our purpose in introducing fragments is to later facilitate the inclusion of a horizontal swipe feature to the ResidenceActivity class.

Preview

In this lab we introduce fragments (Figure 1). Each activity is refactored and replaced by an activity-fragment pair.

Figure 1: Using fragments

ResidenceActivity

Before proceeding with refactoring ResidenceActivity, it is necessary to make some changes as follows:

MyRentApp

Declare a protected MyRentApp field, initialize it in onCreate and provide a getter:

  protected static MyRentApp app;
  @Override
  public void onCreate() {
    ...
    app = this;
    ...
  }

  public static MyRentApp getApp(){
    return app;
  }

Residence (model)

Note that we have already protected Residence against a negative id:

  public Residence() {
    id = unsignedLong();
    ...
  }

  /**
   * Generate a long greater than zero
   * @return Unsigned Long value greater than zero
   */
  private Long unsignedLong() {
    long rndVal = 0;
    do {
      rndVal = new Random().nextLong();
    } while (rndVal <= 0);
    return rndVal;
  }

We will continue building the MyRent app that you developed in the previous lab.

Much of the content of ResidenceActivity from the previous version will be moved to its associated new fragment class, ResidenceFragment.

We now begin refactoring:

  • Replace the contents of ResidenceActivity with the following:
package org.wit.myrent.activities;


import org.wit.myrent.R;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;

public class ResidenceActivity extends AppCompatActivity
{
  ActionBar actionBar;

  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.fragment_container);

    actionBar = getSupportActionBar();

    FragmentManager manager = getSupportFragmentManager();
    Fragment fragment = manager.findFragmentById(R.id.fragmentContainer);
    if (fragment == null)
    {
      fragment = new ResidenceFragment();
      manager.beginTransaction().add(R.id.fragmentContainer, fragment).commit();
    }
  }
}

Add a container for the associated fragment:

Filename: res/layout/fragment_container.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/fragmentContainer"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  />

ResidenceFragment

Here is the new ResidenceFragment class which comprises, mostly, code migrated from ResidenceActivity in the previous version of MyRent:

package org.wit.myrent.activities;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import org.wit.myrent.R;
import org.wit.myrent.app.MyRentApp;
import org.wit.myrent.models.Portfolio;
import org.wit.myrent.models.Residence;

import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.app.DatePickerDialog;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.DatePicker;
import android.widget.EditText;
import static org.wit.android.helpers.ContactHelper.sendEmail;
import static org.wit.android.helpers.ContactHelper.getEmail;
import static org.wit.android.helpers.ContactHelper.getContact;
import static org.wit.android.helpers.IntentHelper.navigateUp;

public class ResidenceFragment extends Fragment implements TextWatcher,
        OnCheckedChangeListener,
        OnClickListener,
        DatePickerDialog.OnDateSetListener
{
    public static   final String  EXTRA_RESIDENCE_ID = "myrent.RESIDENCE_ID";
    private static  final int     REQUEST_CONTACT = 1;

    private EditText geolocation;
    private CheckBox rented;
    private Button   dateButton;
    private Button   tenantButton;
    private Button   reportButton;

    private Residence   residence;
    private Portfolio   portfolio;

    private String emailAddress = "";
    // This field is initialized in `onActivityResult`.
    private Intent data;
    MyRentApp app;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);

        Long resId = (Long) getActivity().getIntent().getSerializableExtra(EXTRA_RESIDENCE_ID);

        app = MyRentApp.getApp();
        portfolio = app.portfolio;
        residence = portfolio.getResidence(resId);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState)
    {
        super.onCreateView(inflater,  parent, savedInstanceState);
        View v = inflater.inflate(R.layout.fragment_residence, parent, false);

        ResidenceActivity residenceActivity = (ResidenceActivity)getActivity();
        residenceActivity.actionBar.setDisplayHomeAsUpEnabled(true);

        addListeners(v);
        updateControls(residence);

        return v;
    }

    private void addListeners(View v)
    {
        geolocation  = (EditText) v.findViewById(R.id.geolocation);
        dateButton   = (Button)   v.findViewById(R.id.registration_date);
        rented       = (CheckBox) v.findViewById(R.id.isrented);
        tenantButton = (Button)   v.findViewById(R.id.tenant);
        reportButton = (Button)   v.findViewById(R.id.residence_reportButton);


        geolocation .addTextChangedListener(this);
        dateButton  .setOnClickListener(this);
        rented      .setOnCheckedChangeListener(this);
        tenantButton.setOnClickListener(this);
        reportButton.setOnClickListener(this);
    }

    public void updateControls(Residence residence)
    {
        geolocation.setText(residence.geolocation);
        rented.setChecked(residence.rented);
        dateButton.setText(residence.getDateString());
        tenantButton.setText("Tenant: "+residence.tenant);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item)
    {
        switch (item.getItemId())
        {
            case android.R.id.home: navigateUp(getActivity());
                return true;

            default:
                return super.onOptionsItemSelected(item);
        }
    }

    @Override
    public void onPause()
    {
        super.onPause();
        portfolio.saveResidences();
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data)
    {
        if (resultCode != Activity.RESULT_OK)
        {
            return;
        }

        switch (requestCode)
        {
            case REQUEST_CONTACT:
                this.data = data;
                checkContactsReadPermission();
                break;
        }
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after)
    { }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count)
    {}

    @Override
    public void afterTextChanged(Editable c)
    {
        Log.i(this.getClass().getSimpleName(), "geolocation " + c.toString());
        residence.geolocation = c.toString();
    }

    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
    {
        residence.rented = isChecked;
    }

    @Override
    public void onClick(View v)
    {
        switch (v.getId())
        {
            case R.id.registration_date      : Calendar c = Calendar.getInstance();
                DatePickerDialog dpd = new DatePickerDialog (getActivity(), this,
                                                c.get(Calendar.YEAR),
                                                c.get(Calendar.MONTH),
                                                c.get(Calendar.DAY_OF_MONTH));
                dpd.show();
                break;
            case R.id.tenant :
                Intent i = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
                startActivityForResult(i, REQUEST_CONTACT);
                tenantButton.setText("Tenant: "+residence.tenant);
                break;
            case R.id.residence_reportButton :
                sendEmail(getActivity(), emailAddress, getString(R.string.residence_report_subject), residence.getResidenceReport(getActivity()));
                break;
        }
    }

    @Override
    public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth)
    {
        Date date = new GregorianCalendar(year, monthOfYear, dayOfMonth).getTime();
        residence.date = date.getTime();
        dateButton.setText(residence.getDateString());
    }

    //https://developer.android.com/training/permissions/requesting.html
    private void checkContactsReadPermission() {
        // Here, thisActivity is the current activity
        if (ContextCompat.checkSelfPermission(getActivity(),
                Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
            //We can request the permission.
            ActivityCompat.requestPermissions(getActivity(),
                    new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_CONTACT);
        }
        else {
            //We already have permission, so go head and read the contact
            readContact();
        }
    }

    private void readContact() {
        String name = getContact(getActivity(), data);
        emailAddress = getEmail(getActivity(), data);
        residence.tenant = name;
        tenantButton.setText("Tenant: "+residence.tenant);
    }

    //https://developer.android.com/training/permissions/requesting.html
    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           String permissions[], int[] grantResults) {
        switch (requestCode) {
            case REQUEST_CONTACT: {
                // If request is cancelled, the result arrays are empty.
                if (grantResults.length > 0
                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    // permission was granted
                    readContact();
                }
            }
        }
    }
}

Rename res/layout/activity_residence.xml to the more appropriate name: fragment_residence.xml.

ResidenceListActivity

As is the case with ResidenceActivity, much of ResidenceListActivity's code is also moved to its new associated fragment class - ResidenceListFragment.

Here is the refactored ResidenceListActivity:

package org.wit.myrent.activities;

import org.wit.myrent.R;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatActivity;

public class ResidenceListActivity extends AppCompatActivity
{
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.fragment_container);

        FragmentManager manager = getSupportFragmentManager();
        Fragment fragment = manager.findFragmentById(R.id.fragmentContainer);
        if (fragment == null)
        {
            fragment = new ResidenceListFragment();
            manager.beginTransaction().add(R.id.fragmentContainer, fragment).commit();
        }
    }
}

ResidenceListFragment

Here is the ResidenceListFragment class which comprises, mostly, code migrated from ResidencyListActivity in the previous version of MyRent:

package org.wit.myrent.activities;

import java.util.ArrayList;

import org.wit.android.helpers.IntentHelper;
import org.wit.myrent.R;
import org.wit.myrent.app.MyRentApp;
import org.wit.myrent.models.Portfolio;
import org.wit.myrent.models.Residence;

import android.widget.ListView;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.TextView;
import android.widget.CheckBox;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.widget.AdapterView.OnItemClickListener;

public class ResidenceListFragment extends ListFragment implements OnItemClickListener
{
    private ArrayList<Residence> residences;
    private Portfolio portfolio;
    private ResidenceAdapter adapter;
    MyRentApp app;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);
        getActivity().setTitle(R.string.app_name);

        app = MyRentApp.getApp();
        portfolio = app.portfolio;
        residences = portfolio.residences;

        adapter = new ResidenceAdapter(getActivity(), residences);
        setListAdapter(adapter);

    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
        View v = super.onCreateView(inflater, parent, savedInstanceState);
        return v;
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        Residence res = ((ResidenceAdapter) getListAdapter()).getItem(position);
        Intent i = new Intent(getActivity(), ResidenceActivity.class);
        i.putExtra(ResidenceFragment.EXTRA_RESIDENCE_ID, res.id);
        startActivityForResult(i, 0);
    }

    @Override
    public void onResume() {
        super.onResume();
        ((ResidenceAdapter) getListAdapter()).notifyDataSetChanged();
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);
        inflater.inflate(R.menu.residencelist, menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.menu_item_new_residence:
                Residence residence = new Residence();
                portfolio.addResidence(residence);

                Intent i = new Intent(getActivity(), ResidenceActivity.class);
                i.putExtra(ResidenceFragment.EXTRA_RESIDENCE_ID, residence.id);
                startActivityForResult(i, 0);
                return true;

            default:
                return super.onOptionsItemSelected(item);
        }
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        Residence residence = adapter.getItem(position);
        IntentHelper.startActivityWithData(getActivity(), ResidenceActivity.class, "RESIDENCE_ID", residence.id);
    }

    class ResidenceAdapter extends ArrayAdapter<Residence>
    {
        private Context context;

        public ResidenceAdapter(Context context, ArrayList<Residence> residences) {
            super(context, 0, residences);
            this.context = context;
        }

        @SuppressLint("InflateParams")
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            if (convertView == null) {
                convertView = inflater.inflate(R.layout.list_item_residence, null);
            }
            Residence res = getItem(position);

            TextView geolocation = (TextView) convertView.findViewById(R.id.residence_list_item_geolocation);
            geolocation.setText(res.geolocation);

            TextView dateTextView = (TextView) convertView.findViewById(R.id.residence_list_item_dateTextView);
            dateTextView.setText(res.getDateString());

            CheckBox rentedCheckBox = (CheckBox) convertView.findViewById(R.id.residence_list_item_isrented);
            rentedCheckBox.setChecked(res.rented);

            return convertView;
        }
    }
}

Build and install the app on a device and verify that it functions correctly.

No major observable difference should exist in the performance of the application as a result of introducing fragments.

We did make one minor change to the "Prospective Tenant" button. The text on the button now states either: - Tenant: None Presently - Tenant: the actual tenant name retrieved from the Residence model

Test

Build and install the app on a phsical device or emulator:

  • Create a new residence
  • Explore the various features.
  • No difference in the behaviour of the app will be obvious (bar the text on the prospective tenant button) as a consequence of introducing fragments.
  • The need for and use of fragments will be demonstrated later in the course.

The application at the end of this lab is available for download from GitHub: android-myrent-2017

Select Releases, followed by V7.0.